package com.jade.editor.object.editors.nodes;

import java.util.ArrayList;

import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Region;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.wb.swt.SWTResourceManager;

import com.jade.editor.object.ani.Anisotropy;
import com.jade.editor.object.editors.ObjectEditor;
import com.jade.logger.Logger;

public class NodeCollector {
	
	public final static int MODE_NONE = 0;
	public final static int MODE_CONNECT = 1;
	public final static int MODE_REMOVE = 3;
	public final static int MODE_ADD = 4;
	public final static int MODE_DISCONNECT = 5;
	public final static int MODE_MOVE = 6;
	public final static int MODE_ADD_N_CONNECT = 7;
	public final static int MODE_ADD_POLE = 8;
	
	private ArrayList<Node> _nlist = new ArrayList<Node>();
	private Pole _pole = null;
	private Composite _parent = null;
	private Node _select1 = null;
	private Node _select2 = null;
	private int _mode = 0;
	private boolean _connection = false;
	private int _xp = 0;
	private int _yp = 0;
	private boolean _fillPoly = false;
	
	private RGB _fillColor = new RGB(153, 204, 255);
	private RGB _connectColor = new RGB(0, 200, 10);
	private RGB _borderColor = new RGB(0, 0, 0);
	private RGB _selectionColor = new RGB(255, 0, 0);
	
	private RGB _poleColor = new RGB(0, 0, 0);
	private RGB _poleSelectionColor = new RGB(255, 0, 0);
	private ObjectEditor _editor = null;
	
	private Anisotropy _ani = null;
	
	public void setAnisotropy(Anisotropy a){
		_ani = a;
	}
	
	public void setNodeList(ArrayList<Node> nlist){
		if(_nlist.size() > 0)
			_nlist.clear();
		_nlist = nlist;
		
		for(Node n : _nlist)
			n.init(_parent, this, _borderColor, _selectionColor);
		
	}
	
	public Point getPole(){
		if (_pole != null)
			return new Point(_pole.x, _pole.y);
		return null;
	}
	
	public void setPole(Point p){
		_pole = new Pole(_parent, this, p.x, p.y,_poleColor, _poleSelectionColor);
	}
	
	public ArrayList<Node> getNodeList(){
		return _nlist;
	}
	
	public RGB getPoleColor(){
		return _poleColor;
	}
	
	public RGB getPoleSelectionColor(){
		return _poleSelectionColor;
	}
	
	public void setPoleSelectionColor(RGB color){
		_poleSelectionColor = color;
		if (_pole != null)
			_pole.setSelectionColor(color);
	}
	
	public void setPoleColor(RGB color){
		_poleColor = color;
		if(_pole != null)
			_pole.setColor(color);
	}
	
	public boolean isFilled(){
		return _fillPoly;
	}
	
	public void clear(){
		for(Node n : _nlist)
			n.punish();
		_nlist.clear();
		if(_pole != null)
			_pole.punish();
		_pole = null;
		_editor.setDirty(true);
		_parent.redraw();
	}
	
	public void fillPolygon(boolean b){
		_fillPoly = b;
		_parent.redraw();
	}
	
	public void setFillColor(RGB color){
		_fillColor = color;
		_parent.redraw();
	}
	
	public void setConnectionColor(RGB color){
		_connectColor = color;
	}
	
	public void setBorderColor(RGB color){
		_borderColor = color;
		_parent.redraw();
		
		for(Node n : _nlist)
			n.setColor(color);
	}
	
	public void setSelectionColor(RGB color){
		_selectionColor = color;
		
		for(Node n : _nlist)
			n.setSelectionColor(color);
	}
	
	public RGB getFillColor(){
		return _fillColor;
	}
	
	public RGB getConnectionColor(){
		return _connectColor;
	}
	
	public RGB getBorderColor(){
		return _borderColor;
	}
	
	public RGB getSelectionColor(){
		return _selectionColor;
	}
	
	public void setParent(Composite parent){
		_parent = parent;
		addListeners();
		_mode = MODE_MOVE;
	}
	
	public NodeCollector(Composite parent, ObjectEditor editor){
		_parent = parent;
		_editor = editor;
		addListeners();
		_mode = MODE_MOVE;
	}
	
	public void addNode(int x, int y){
		
		if (_mode == MODE_ADD_POLE && checkNodes()){
			
			Pole tmp = new Pole(_parent, this, x, y, _poleColor, _poleSelectionColor);
			if (checkInRegion(tmp.x, tmp.y)){
				
				if (_pole != null){
					_pole.punish();
					_pole = null;
				}
				
				_pole = new Pole(_parent, this, x, y, _poleColor, _poleSelectionColor);
			}

			tmp.punish();
			tmp = null;	
			_editor.setDirty(true);
		}
		
		if (_mode == MODE_ADD){
			Node n = new Node(_parent, this,  x, y, _borderColor, _selectionColor);
			if (!checkRadius(n))
				_nlist.add(n);
			else
				n.punish();
			_editor.setDirty(true);
		}
		
		if (_mode == MODE_ADD_N_CONNECT){
			Node n = new Node(_parent, this,  x, y, _borderColor, _selectionColor);
			if (!checkRadius(n)){
				
				if (_nlist.size() > 0){
					if(!checkIntersection(n, _nlist.get(_nlist.size()-1))){
						_nlist.add(n);
						
						if (_nlist.size() > 1){
							
							_nlist.get(_nlist.size()-2).setOutNode(n);
							n.setInNode(_nlist.get(_nlist.size()-2));
							
							if (_ani != null && checkNodes())
								_ani.reCulc();
							
							_parent.redraw();
						}
						
					}
					else
						n.punish();
				}
				else
					_nlist.add(n);
				
			}
			else
				n.punish();
			
			_editor.setDirty(true);
			if (_ani != null && checkNodes())
				_ani.reCulc();
		}
			
			
		
	}
	
	public void select(Node b){
		if (_mode == MODE_REMOVE){
			_nlist.remove(b);
			b.punish();
			if (_pole != null)
				_pole.punish();
			
			_editor.setDirty(true);
			if (_ani != null && checkNodes())
				_ani.reCulc();
		}
		
		if (_mode == MODE_CONNECT){
			if (_select1 == null){
				_select1 = b;
				_xp = b.x;
				_yp = b.y;
				_connection = true;
			}
			else if (_select2 == null && !b.equals(_select1)){
				_select2 = b;
				
				if (_select1.hasOutNode()){
					if (_select1.getOutNode().hasInNode())
						_select1.getOutNode().removeInNode();
					_select1.removeOutNode();
				}
				_select1.setOutNode(_select2);
				
				if (_select2.hasInNode()){
					if (_select2.getInNode().hasOutNode())
						_select2.getInNode().removeOutNode();
					_select2.removeInNode();
				}
				_select2.setInNode(_select1);
				
				if (checkIntersection(_select1, _select2)){
					_select1.removeOutNode();
					_select2.removeInNode();
					Logger.print(Logger.STATE_WARNING, "Lines cannot intersect");
					
				}
				else
					_parent.redraw();
				
				if (_ani != null && checkNodes())
					_ani.reCulc();
				
				_select1 = null;
				_select2 = null;
				_connection = false;
				_editor.setDirty(true);
			}
		}
		
		if (_mode == MODE_DISCONNECT){
			if (b.hasInNode() && b.getInNode().hasOutNode()){
				b.getInNode().removeOutNode();
				b.removeInNode();
				if (_pole != null)
					_pole.punish();
				_editor.setDirty(true);
				
				if (_ani != null && checkNodes())
					_ani.reCulc();
			}
			
			if (b.hasOutNode() && b.getOutNode().hasInNode()){
				b.getOutNode().removeInNode();
				b.removeOutNode();
				if (_pole != null)
					_pole.punish();
			}

			_parent.redraw();
			_editor.setDirty(true);
		}
		
		if (_mode == MODE_MOVE){
			boolean back = false;
			if(b.hasInNode()){
				if (checkIntersection(b.getInNode(), b))
					back = true;
			}
			if(b.hasOutNode()){
				if (checkIntersection(b, b.getOutNode()))
					back = true;
			}
			if(checkAngle(b))
				back = true;
			if(checkRadius(b))
				back = true;
			
			if(back) b.stepBack();
			
			if (_pole != null){
				if(!checkInRegion(_pole.x, _pole.y)){
					_pole.punish();
					_pole = null;
				}
			}
			_editor.setDirty(true);
			if (_ani != null && checkNodes())
				_ani.reCulc();
			
		}
		
	}
	
	
	public void preSelect(Node b){
		if (_mode == MODE_MOVE){
			b.setAllowMove(true);
		}
		else
			b.setAllowMove(false);
	}
	
	public void preSelect(Pole p){
		if(_mode == MODE_MOVE){
			p.setMoveAllowed(true);
		}
		else{
			p.setMoveAllowed(false);
		}
		
	}
	
	public void select(Pole p){
		if (_mode == MODE_REMOVE){
			p.punish();
			if (_pole != null){
				_pole = null;
			}
			_editor.setDirty(true);
		}
		if (_mode == MODE_MOVE){
			if (!checkInRegion(p.x, p.y))
				p.stepBack();
			_editor.setDirty(true);
		}
	}
	
	
	/**
	 * Check existing of pole
	 * @return <code>true</code> if pole exists
	 */
	public boolean hasPole(){
		if (_pole != null)
			return true;
		return false;
	}
	
	
	/**
	 * This method calculates distance from x axis to nearest point by x
	 * and distance from y axis to nearest point by y
	 * @return <code>Point</code> where x distance to nearest x point, and y distance to nearest y point
	 */
	public Point getNullDistance(){
		int x = 0;
		int y = 0;
		
		if(_nlist.size() > 0){
			x = _nlist.get(0).x;
			y = _nlist.get(0).y;
			
			for(Node n : _nlist){
				if (x > n.x)
					x = n.x;
				if (y > n.y)
					y = n.y;
			}
			
		}
		
		return new Point(x,y);
	}
	
	
	/**
	 * This method calculates points moved by distance to nearest axis by x and y
	 * @return Array of moved points for distance to nearest axis
	 */
	public ArrayList<Point> getNodesNulledDistance(){
		ArrayList<Point> plist = new ArrayList<Point>();
		Point dist = getNullDistance();
		
		for(Node n : _nlist)
			plist.add(new Point(n.x - dist.x, n.y - dist.y));
		return plist;
	}
	
	
	/**
	 * This method calculates pole point moved by distance to nearest axis by x and y
	 * @return Point moved to nearest axis
	 */
	public Point getPoleNulledDistance(){
		
		if (_pole != null){
			Point dist = getNullDistance();
			return new Point(_pole.x - dist.x, _pole.y - dist.y);
		}
		
		return null;
	}
	
	
	/**
	 * This method sets the mode of NodeCollector
	 * allowed to use next constants:
	 * <code>MODE_NONE</code>
	 * <code>MODE_CONNECT</code>
	 * <code>MODE_REMOVE</code>
	 * <code>MODE_ADD</code>
	 * <code>MODE_DISCONNECT</code>
	 * <code>MODE_MOVE</code>
	 * <code>MODE_ADD_N_CONNECT</code>
	 * <code>MODE_ADD_POLE</code>
	 * 
	 * @param mode integer value of mode
	 */
	public void setMode(int mode){
		_mode = mode;
	}
	
	
	/**
	 * This method checks all node to be connected
	 * @return <code>true</code> if all node are connected
	 */
	public boolean checkNodes(){
		for(Node n : _nlist){
			if (!n.hasInNode() || !n.hasOutNode())
				return false;
		}
		
		if (_nlist.size() == 0)
			return false;
		
		return true;
	}
	
	
	public void paint(PaintEvent e){
		
		Color tmpBG = e.gc.getBackground();
		Color tmpFG = e.gc.getForeground();
		
		if(_fillPoly && checkNodes()){
			e.gc.setBackground(SWTResourceManager.getColor(_fillColor));
			
			e.gc.fillPolygon(getCoordsData());
		}
		
		e.gc.setForeground(SWTResourceManager.getColor(_borderColor));
		for(Node n : _nlist){
			if(n.hasOutNode())
				e.gc.drawLine(n.x, n.y, n.getOutNode().x, n.getOutNode().y);
		}
		
		if(_connection){
			e.gc.setForeground(SWTResourceManager.getColor(_connectColor));
			e.gc.drawLine(_select1.x, _select1.y, _xp, _yp);
		}
		
		e.gc.setBackground(tmpBG);
		e.gc.setForeground(tmpFG);
	}
	
	/**
	 * This method adds event listeners to {@link #_parent}
	 */
	private void addListeners(){		
		_parent.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseDown(MouseEvent e) {
				_select1 = null;
				_connection = false;
				_parent.redraw();
			}
		});
		
		_parent.addMouseMoveListener(new MouseMoveListener() {
			public void mouseMove(MouseEvent e) {
				if (_connection){
					_xp = e.x;
					_yp = e.y;
					_parent.redraw();
				}
			}
		});
		
	}
	
	
	/**
	 * Checks intersection of line created by two nodes with others
	 * @param n1 First end of line
	 * @param n2 Second end of line
	 * @return <code>true</code> if lines are intersects
	 */
	private boolean checkIntersection(Node n1, Node n2){
		int ax1 = n1.x;	//1st line
		int ax2 = n2.x;
		int ay1 = n1.y;
		int ay2 = n2.y;
		for (Node n : _nlist){
			if(n.hasOutNode()){
				int bx1 = n.x;	//2nd line
				int bx2 = n.getOutNode().x;
				int by1 = n.y;
				int by2 = n.getOutNode().y;
				
				int v1 = ((bx2 - bx1) * (ay1 - by1)) - ((by2 - by1) * (ax1 - bx1));
				int v2 = ((bx2 - bx1) * (ay2 - by1)) - ((by2 - by1) * (ax2 - bx1));
				int v3 = ((ax2 - ax1) * (by1 - ay1)) - ((ay2 - ay1) * (bx1 - ax1));
				int v4 = ((ax2 - ax1) * (by2 - ay1)) - ((ay2 - ay1) * (bx2 - ax1));
				
				if ((v1 * v2) < 0 && (v3 * v4) < 0){
					return true;
				}
			}
		}
		return false;
	}
	
	
	/**
	 * Checks angle of Node and its out and in nodes
	 * Change value of <code>allowedAngle</code>, 10 degrees by default
	 * @param n1 Node to check
	 * @return <code>true</code> if angle less than 10 degrees
	 */
	private boolean checkAngle(Node n1){
		double angle = 0.0;
		double allowedAngle = 10.0;
		Node n2 = null;
		Node n3 = null;
		
		if(n1.hasInNode() && !n1.hasOutNode()){
			n2 = n1.getInNode();
			if (n2.hasInNode()){
				n3 = n2.getInNode();			
			}
		}
		
		if(n1.hasOutNode() && !n1.hasInNode()){
			n2 = n1.getOutNode();
			if (n2.hasOutNode()){
				n3 = n2.getOutNode();
			}
		}
		
		if(n1.hasInNode() && n1.hasOutNode()){
			int x1 = n1.getInNode().x;
			int y1 = n1.getInNode().y;
			
			int x2 = n1.x;
			int y2 = n1.y;
			
			int x3 = n1.getOutNode().x;
			int y3 = n1.getOutNode().y;
			
			double T = (x1 - x2) * (x3 - x2) + (y1 - y2) * (y3 - y2);
			double Ma = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
			double Mb = Math.sqrt((x2 - x3) * (x2 - x3) + (y2 - y3) * (y2 - y3));
			angle = Math.acos(T / (Ma * Mb));
			angle = angle * 180 / Math.PI;
			if (angle < allowedAngle){
				
				Logger.print(Logger.STATE_WARNING, "Too small angle");
				return true;
			}
		}
		
		if(n1.hasInNode() && n1.getInNode().hasInNode()){
			int x1 = n1.getInNode().getInNode().x;
			int y1 = n1.getInNode().getInNode().y;
			
			int x2 = n1.getInNode().x;
			int y2 = n1.getInNode().y;
			
			int x3 = n1.x;
			int y3 = n1.y;
			
			double T = (x1 - x2) * (x3 - x2) + (y1 - y2) * (y3 - y2);
			double Ma = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
			double Mb = Math.sqrt((x2 - x3) * (x2 - x3) + (y2 - y3) * (y2 - y3));
			angle = Math.acos(T / (Ma * Mb));
			angle = angle * 180 / Math.PI;
			if (angle < allowedAngle){
				
				Logger.print(Logger.STATE_WARNING, "Too small angle");
				return true;
			}
		}
		
		if(n1.hasOutNode() && n1.getOutNode().hasOutNode()){
			int x1 = n1.x;
			int y1 = n1.y;
			
			int x2 = n1.getOutNode().x;
			int y2 = n1.getOutNode().y;
			
			int x3 = n1.getOutNode().getOutNode().x;
			int y3 = n1.getOutNode().getOutNode().y;
			
			double T = (x1 - x2) * (x3 - x2) + (y1 - y2) * (y3 - y2);
			double Ma = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
			double Mb = Math.sqrt((x2 - x3) * (x2 - x3) + (y2 - y3) * (y2 - y3));
			angle = Math.acos(T / (Ma * Mb));
			angle = angle * 180 / Math.PI;
			if (angle < allowedAngle){
				
				Logger.print(Logger.STATE_WARNING, "Too small angle");
				return true;
			}
		}
		
		if(n2 != null && n3 !=null){
			double T = (n1.x - n2.x) * (n3.x - n2.x) + (n1.y - n2.y) * (n3.y - n2.y);
			double Ma = Math.sqrt((n1.x - n2.x) * (n1.x - n2.x) + (n1.y - n2.y) * (n1.y - n2.y));
			double Mb = Math.sqrt((n2.x - n3.x) * (n2.x - n3.x) + (n2.y - n3.y) * (n2.y - n3.y));
			angle = Math.acos(T / (Ma * Mb));
			angle = angle * 180 / Math.PI;
			if (angle < allowedAngle){
				
				Logger.print(Logger.STATE_WARNING, "Too small angle");
				return true;
			}
		}
		
		return false;
	}
	
	
	/**
	 * Checks node position about others node in distance of 10px
	 * change <code>allowedDistance</code> parameter inside method to set new distance
	 * @param n1 Node to check
	 * @return <code>true</code> if distance less then 10px
	 */
	private boolean checkRadius(Node n1){
		double allowedDistance = 10.0;
		for (Node n : _nlist){
			if (n.equals(n1)) continue;
			double d = Math.sqrt((n.x - n1.x) * (n.x - n1.x) + (n.y - n1.y) * (n.y - n1.y));
			if (d < allowedDistance) 
				return true;
		}
		
		return false;
	}
	
	
	/**
	 * This method creates array of coordinates of all created nodes
	 * check with {@link #checkNodes()} before use this method
	 * @return array of coordinates [x,y...x,y]
	 */
	public int[] getCoordsData(){
		
		int[] coords = new int[_nlist.size() * 2];
		
		Node n = _nlist.get(0);
		Node next = n.getOutNode();
		int count = 2;
		
		coords[0] = n.x;
		coords[1] = n.y;
		
		while(n != next){
			coords[count] = next.x;
			count++;
			coords[count] = next.y;
			count++;
			next = next.getOutNode();
		}
		
		return coords;
	}
	
	
	/**
	 * This method creates region by created and connected nodes
	 * check with {@link #checkNodes()} before use this method
	 * @return Region created by connected nodes
	 */
	private Region getRegion(){
		Region r = new Region();
		r.add(getCoordsData());
		return r;
	}
	
	
	/**
	 * This method checks position of point about region
	 * created by connected nodes
	 * @param x X position of point
	 * @param y Y position of point
	 * @return true if point inside region
	 */
	private boolean checkInRegion(int x, int y){
		return getRegion().contains(x, y);
	}
	
	
}
